#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
视频文件处理器模块，提供视频文件的读取、保存、转换、剪辑等功能
"""

import os
import io
import tempfile
import shutil
from pathlib import Path
from typing import Union, List, Dict, Optional, Tuple, Any, BinaryIO

from ..exception.exceptions import FileException
from ..logger.logger import get_logger
from ..exception.decorators import exception_handler

logger = get_logger(__name__)


class VideoFileHandler:
    """
    视频文件处理器，提供视频文件的读取、保存、转换、剪辑等功能
    
    该类需要安装opencv-python库：pip install opencv-python
    对于某些功能，还需要安装ffmpeg
    
    Attributes:
        None
    """
    
    def __init__(self):
        """
        初始化视频文件处理器
        
        Raises:
            ImportError: 如果未安装opencv-python库则抛出
        """
        try:
            import cv2
            self.cv2 = cv2
            logger.info("视频文件处理器初始化成功")
        except ImportError:
            raise ImportError("使用VideoFileHandler需要安装opencv-python库: pip install opencv-python")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def load_video(self, video_path: Union[str, Path]) -> Any:
        """
        加载视频文件
        
        Args:
            video_path: 视频文件路径
            
        Returns:
            cv2.VideoCapture: 视频捕获对象
            
        Raises:
            FileException: 文件不存在或加载失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> video = handler.load_video('example.mp4')
            >>> print(f"视频帧数: {int(video.get(cv2.CAP_PROP_FRAME_COUNT))}")
        """
        video_path = Path(video_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        try:
            # 加载视频文件
            video = self.cv2.VideoCapture(str(video_path))
            
            # 检查视频是否成功打开
            if not video.isOpened():
                raise FileException(f"无法打开视频文件: {video_path}")
            
            logger.debug(f"视频文件加载成功: {video_path}")
            return video
        except Exception as e:
            raise FileException(f"加载视频文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def get_video_info(self, video_path: Union[str, Path]) -> Dict[str, Any]:
        """
        获取视频信息
        
        Args:
            video_path: 视频文件路径
            
        Returns:
            Dict[str, Any]: 包含视频信息的字典，包括帧数、帧率、分辨率、时长等
            
        Raises:
            FileException: 文件不存在或获取信息失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> info = handler.get_video_info('example.mp4')
            >>> print(f"视频分辨率: {info['width']}x{info['height']}")
            >>> print(f"视频时长: {info['duration']} 秒")
        """
        # 加载视频
        video = self.load_video(video_path)
        
        try:
            # 获取基本信息
            frame_count = int(video.get(self.cv2.CAP_PROP_FRAME_COUNT))
            fps = video.get(self.cv2.CAP_PROP_FPS)
            width = int(video.get(self.cv2.CAP_PROP_FRAME_WIDTH))
            height = int(video.get(self.cv2.CAP_PROP_FRAME_HEIGHT))
            
            # 计算时长（秒）
            duration = frame_count / fps if fps > 0 else 0
            
            # 获取编解码器信息
            fourcc_int = int(video.get(self.cv2.CAP_PROP_FOURCC))
            fourcc = ""
            for i in range(4):
                fourcc += chr((fourcc_int >> (8 * i)) & 0xFF)
            
            # 获取文件大小
            video_path = Path(video_path)
            file_size = video_path.stat().st_size
            
            # 释放视频对象
            video.release()
            
            info = {
                'frame_count': frame_count,
                'fps': fps,
                'width': width,
                'height': height,
                'resolution': f"{width}x{height}",
                'duration': duration,
                'duration_formatted': self._format_duration(duration),
                'codec': fourcc,
                'file_size': file_size,
                'file_size_formatted': self._format_file_size(file_size),
                'format': video_path.suffix.lstrip('.').lower()
            }
            
            return info
        except Exception as e:
            # 确保视频对象被释放
            video.release()
            raise FileException(f"获取视频信息失败: {e}")
    
    def _format_duration(self, seconds: float) -> str:
        """
        将秒数格式化为时:分:秒格式
        
        Args:
            seconds: 秒数
            
        Returns:
            str: 格式化后的时长字符串
        """
        hours = int(seconds // 3600)
        minutes = int((seconds % 3600) // 60)
        seconds = int(seconds % 60)
        return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
    
    def _format_file_size(self, size_bytes: int) -> str:
        """
        将字节数格式化为人类可读的文件大小
        
        Args:
            size_bytes: 字节数
            
        Returns:
            str: 格式化后的文件大小字符串
        """
        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if size_bytes < 1024.0 or unit == 'TB':
                break
            size_bytes /= 1024.0
        return f"{size_bytes:.2f} {unit}"
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def extract_frame(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                     frame_number: int = 0, quality: int = 95) -> None:
        """
        从视频中提取指定帧并保存为图像
        
        Args:
            video_path: 视频文件路径
            output_path: 输出图像文件路径
            frame_number: 要提取的帧号（从0开始），默认为0（第一帧）
            quality: 图像质量（1-100），默认为95
            
        Raises:
            FileException: 文件不存在、帧不存在或提取失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 提取第一帧
            >>> handler.extract_frame('example.mp4', 'frame0.jpg')
            >>> # 提取第100帧
            >>> handler.extract_frame('example.mp4', 'frame100.jpg', frame_number=100)
        """
        # 加载视频
        video = self.load_video(video_path)
        
        try:
            # 获取总帧数
            frame_count = int(video.get(self.cv2.CAP_PROP_FRAME_COUNT))
            
            # 检查帧号是否有效
            if frame_number < 0 or frame_number >= frame_count:
                raise FileException(f"帧号{frame_number}超出范围，视频有{frame_count}帧")
            
            # 设置视频位置到指定帧
            video.set(self.cv2.CAP_PROP_POS_FRAMES, frame_number)
            
            # 读取帧
            ret, frame = video.read()
            if not ret:
                raise FileException(f"无法读取帧{frame_number}")
            
            # 确保输出目录存在
            output_path = Path(output_path)
            output_path.parent.mkdir(parents=True, exist_ok=True)
            
            # 保存帧为图像
            success = self.cv2.imwrite(str(output_path), frame, [self.cv2.IMWRITE_JPEG_QUALITY, quality])
            if not success:
                raise FileException(f"无法保存帧到{output_path}")
            
            logger.info(f"视频帧提取成功: {video_path} -> {output_path} (帧: {frame_number})")
        except Exception as e:
            raise FileException(f"提取视频帧失败: {e}")
        finally:
            # 释放视频对象
            video.release()
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def extract_frames(self, video_path: Union[str, Path], output_dir: Union[str, Path], 
                      interval: int = 1, prefix: str = "frame", format: str = "jpg", 
                      quality: int = 95, max_frames: int = None) -> int:
        """
        从视频中提取多个帧并保存为图像
        
        Args:
            video_path: 视频文件路径
            output_dir: 输出目录路径
            interval: 提取帧的间隔（每隔多少帧提取一帧），默认为1（提取所有帧）
            prefix: 输出文件名前缀，默认为"frame"
            format: 输出图像格式，默认为"jpg"
            quality: 图像质量（1-100），默认为95
            max_frames: 最多提取的帧数，默认为None（不限制）
            
        Returns:
            int: 提取的帧数
            
        Raises:
            FileException: 文件不存在或提取失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 提取所有帧
            >>> count = handler.extract_frames('example.mp4', 'frames/')
            >>> # 每隔10帧提取一帧，最多提取20帧
            >>> count = handler.extract_frames('example.mp4', 'frames/', interval=10, max_frames=20)
        """
        # 加载视频
        video = self.load_video(video_path)
        
        try:
            # 确保输出目录存在
            output_dir = Path(output_dir)
            output_dir.mkdir(parents=True, exist_ok=True)
            
            # 获取总帧数
            frame_count = int(video.get(self.cv2.CAP_PROP_FRAME_COUNT))
            
            # 计算要提取的帧数
            frames_to_extract = (frame_count + interval - 1) // interval
            if max_frames is not None and frames_to_extract > max_frames:
                frames_to_extract = max_frames
            
            # 提取帧
            extracted_count = 0
            frame_index = 0
            
            while extracted_count < frames_to_extract:
                # 设置视频位置到指定帧
                video.set(self.cv2.CAP_PROP_POS_FRAMES, frame_index)
                
                # 读取帧
                ret, frame = video.read()
                if not ret:
                    break
                
                # 保存帧为图像
                output_path = output_dir / f"{prefix}_{extracted_count:06d}.{format}"
                success = self.cv2.imwrite(str(output_path), frame, [self.cv2.IMWRITE_JPEG_QUALITY, quality])
                if not success:
                    raise FileException(f"无法保存帧到{output_path}")
                
                extracted_count += 1
                frame_index += interval
                
                # 检查是否达到最大帧数
                if max_frames is not None and extracted_count >= max_frames:
                    break
            
            logger.info(f"视频帧批量提取成功: {video_path} -> {output_dir} (提取了 {extracted_count} 帧)")
            return extracted_count
        except Exception as e:
            raise FileException(f"批量提取视频帧失败: {e}")
        finally:
            # 释放视频对象
            video.release()
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_video_from_frames(self, frames_dir: Union[str, Path], output_path: Union[str, Path], 
                               fps: float = 30.0, pattern: str = "*.jpg", 
                               codec: str = "mp4v", sort_frames: bool = True) -> None:
        """
        从图像序列创建视频
        
        Args:
            frames_dir: 包含图像帧的目录路径
            output_path: 输出视频文件路径
            fps: 帧率，默认为30.0
            pattern: 图像文件匹配模式，默认为"*.jpg"
            codec: 视频编解码器，默认为"mp4v"（MP4）
            sort_frames: 是否对图像文件进行排序，默认为True
            
        Raises:
            FileException: 目录不存在、没有找到图像或创建失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> handler.create_video_from_frames('frames/', 'output.mp4')
            >>> # 使用不同的帧率和编解码器
            >>> handler.create_video_from_frames('frames/', 'output.avi', fps=24.0, pattern='*.png', codec='XVID')
        """
        frames_dir = Path(frames_dir)
        
        if not frames_dir.exists() or not frames_dir.is_dir():
            raise FileException(f"图像帧目录不存在: {frames_dir}")
        
        try:
            # 获取所有匹配的图像文件
            import glob
            frame_files = glob.glob(str(frames_dir / pattern))
            
            if not frame_files:
                raise FileException(f"在目录{frames_dir}中没有找到匹配{pattern}的图像文件")
            
            # 对文件进行排序
            if sort_frames:
                frame_files.sort()
            
            # 读取第一帧以获取图像尺寸
            first_frame = self.cv2.imread(frame_files[0])
            if first_frame is None:
                raise FileException(f"无法读取图像文件: {frame_files[0]}")
            
            height, width, _ = first_frame.shape
            
            # 确保输出目录存在
            output_path = Path(output_path)
            output_path.parent.mkdir(parents=True, exist_ok=True)
            
            # 创建视频写入器
            fourcc = self.cv2.VideoWriter_fourcc(*codec)
            video_writer = self.cv2.VideoWriter(str(output_path), fourcc, fps, (width, height))
            
            # 将每一帧写入视频
            for frame_file in frame_files:
                frame = self.cv2.imread(frame_file)
                if frame is not None:
                    video_writer.write(frame)
            
            # 释放视频写入器
            video_writer.release()
            
            logger.info(f"从图像帧创建视频成功: {frames_dir} -> {output_path} (帧数: {len(frame_files)})")
        except Exception as e:
            raise FileException(f"从图像帧创建视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def convert_video(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                     codec: str = None, fps: float = None, resolution: Tuple[int, int] = None, 
                     bitrate: str = None) -> None:
        """
        转换视频格式、编解码器、分辨率或帧率
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            codec: 输出视频编解码器，默认为None（根据输出文件扩展名自动确定）
            fps: 输出视频帧率，默认为None（保持原帧率）
            resolution: 输出视频分辨率(宽, 高)，默认为None（保持原分辨率）
            bitrate: 输出视频比特率，如"2M"，默认为None（由编解码器自动确定）
            
        Raises:
            FileException: 文件不存在或转换失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 转换格式
            >>> handler.convert_video('input.mp4', 'output.avi')
            >>> # 转换分辨率和帧率
            >>> handler.convert_video('input.mp4', 'output.mp4', resolution=(1280, 720), fps=30.0)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 构建ffmpeg命令
            cmd = ["ffmpeg", "-y", "-i", str(video_path)]
            
            # 添加分辨率参数
            if resolution is not None:
                cmd.extend(["-s", f"{resolution[0]}x{resolution[1]}"])
            
            # 添加帧率参数
            if fps is not None:
                cmd.extend(["-r", str(fps)])
            
            # 添加比特率参数
            if bitrate is not None:
                cmd.extend(["-b:v", bitrate])
            
            # 添加编解码器参数
            if codec is not None:
                cmd.extend(["-c:v", codec])
            
            # 添加输出文件路径
            cmd.append(str(output_path))
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频转换成功: {video_path} -> {output_path}")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"转换视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def trim_video(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                  start_time: float, end_time: float, copy_codec: bool = True) -> None:
        """
        剪辑视频
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            start_time: 起始时间（秒）
            end_time: 结束时间（秒）
            copy_codec: 是否直接复制编解码器（不重新编码），默认为True
            
        Raises:
            FileException: 文件不存在或剪辑失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 剪辑从10秒到30秒的部分
            >>> handler.trim_video('input.mp4', 'output.mp4', 10.0, 30.0)
            >>> # 剪辑并重新编码
            >>> handler.trim_video('input.mp4', 'output.mp4', 10.0, 30.0, copy_codec=False)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-ss", str(start_time),
                "-i", str(video_path),
                "-t", str(end_time - start_time)
            ]
            
            # 如果直接复制编解码器
            if copy_codec:
                cmd.extend(["-c", "copy"])
            
            # 添加输出文件路径
            cmd.append(str(output_path))
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频剪辑成功: {video_path} -> {output_path} ({start_time}s - {end_time}s)")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"剪辑视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def concatenate_videos(self, video_paths: List[Union[str, Path]], output_path: Union[str, Path], 
                         method: str = "concat") -> None:
        """
        连接多个视频文件
        
        Args:
            video_paths: 输入视频文件路径列表
            output_path: 输出视频文件路径
            method: 连接方法，可选"concat"（直接连接，要求相同编解码器和分辨率）或"transcode"（转码连接，可处理不同格式），默认为"concat"
            
        Raises:
            FileException: 文件不存在或连接失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 直接连接相同格式的视频
            >>> handler.concatenate_videos(['part1.mp4', 'part2.mp4', 'part3.mp4'], 'combined.mp4')
            >>> # 转码连接不同格式的视频
            >>> handler.concatenate_videos(['part1.mp4', 'part2.avi'], 'combined.mp4', method='transcode')
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        if not video_paths:
            raise FileException("视频文件路径列表不能为空")
        
        # 检查所有视频文件是否存在
        for path in video_paths:
            path = Path(path)
            if not path.exists():
                raise FileException(f"视频文件不存在: {path}")
        
        output_path = Path(output_path)
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            if method == "concat":
                # 创建临时文件列表
                temp_dir = tempfile.mkdtemp()
                list_file_path = Path(temp_dir) / "file_list.txt"
                
                # 写入文件列表
                with open(list_file_path, "w") as f:
                    for path in video_paths:
                        f.write(f"file '{Path(path).absolute()}\n'")
                
                # 构建ffmpeg命令
                cmd = [
                    "ffmpeg", "-y",
                    "-f", "concat",
                    "-safe", "0",
                    "-i", str(list_file_path),
                    "-c", "copy",
                    str(output_path)
                ]
                
                # 执行ffmpeg命令
                process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
                
                # 清理临时文件
                try:
                    os.remove(list_file_path)
                    os.rmdir(temp_dir)
                except Exception:
                    pass  # 忽略清理错误
                
            elif method == "transcode":
                # 创建临时目录
                temp_dir = tempfile.mkdtemp()
                
                # 转换所有视频为相同格式的临时文件
                temp_files = []
                for i, path in enumerate(video_paths):
                    temp_file = Path(temp_dir) / f"temp_{i}.ts"
                    temp_files.append(temp_file)
                    
                    # 转换为TS格式（便于连接）
                    cmd = [
                        "ffmpeg", "-y",
                        "-i", str(path),
                        "-c", "copy",
                        "-bsf:v", "h264_mp4toannexb",
                        "-f", "mpegts",
                        str(temp_file)
                    ]
                    
                    process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
                
                # 连接临时文件
                concat_list = "|".join([str(f) for f in temp_files])
                cmd = [
                    "ffmpeg", "-y",
                    "-i", f"concat:{concat_list}",
                    "-c", "copy",
                    "-bsf:a", "aac_adtstoasc",
                    str(output_path)
                ]
                
                process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
                
                # 清理临时文件
                try:
                    for temp_file in temp_files:
                        os.remove(temp_file)
                    os.rmdir(temp_dir)
                except Exception:
                    pass  # 忽略清理错误
                
            else:
                raise ValueError(f"不支持的连接方法: {method}，支持的方法有'concat'和'transcode'")
            
            logger.info(f"视频连接成功: {len(video_paths)} 个文件 -> {output_path}")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"连接视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def extract_audio(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                    format: str = None, bitrate: str = "192k") -> None:
        """
        从视频中提取音频
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出音频文件路径
            format: 输出音频格式，默认为None（根据输出文件扩展名自动确定）
            bitrate: 音频比特率，默认为"192k"
            
        Raises:
            FileException: 文件不存在或提取失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> handler.extract_audio('input.mp4', 'audio.mp3')
            >>> handler.extract_audio('input.mp4', 'audio.aac', bitrate='256k')
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 如果未指定格式，根据文件扩展名确定
            if format is None:
                format = output_path.suffix.lstrip('.').lower()
                if not format:
                    format = 'mp3'  # 默认格式
            
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-vn",  # 不包含视频
                "-ar", "44100",  # 采样率
                "-ab", bitrate,  # 比特率
                "-f", format,
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频音频提取成功: {video_path} -> {output_path}")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"提取视频音频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def replace_audio(self, video_path: Union[str, Path], audio_path: Union[str, Path], 
                    output_path: Union[str, Path], copy_video: bool = True) -> None:
        """
        替换视频中的音频
        
        Args:
            video_path: 输入视频文件路径
            audio_path: 输入音频文件路径
            output_path: 输出视频文件路径
            copy_video: 是否直接复制视频流（不重新编码），默认为True
            
        Raises:
            FileException: 文件不存在或替换失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> handler.replace_audio('input.mp4', 'audio.mp3', 'output.mp4')
            >>> # 替换音频并重新编码视频
            >>> handler.replace_audio('input.mp4', 'audio.mp3', 'output.mp4', copy_video=False)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        audio_path = Path(audio_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        if not audio_path.exists():
            raise FileException(f"音频文件不存在: {audio_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-i", str(audio_path),
                "-map", "0:v",  # 使用第一个输入（视频）的视频流
                "-map", "1:a",  # 使用第二个输入（音频）的音频流
            ]
            
            # 如果直接复制视频流
            if copy_video:
                cmd.extend(["-c:v", "copy"])
            
            # 添加输出文件路径
            cmd.append(str(output_path))
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频音频替换成功: {video_path} + {audio_path} -> {output_path}")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"替换视频音频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def add_subtitle(self, video_path: Union[str, Path], subtitle_path: Union[str, Path], 
                   output_path: Union[str, Path], embed: bool = False) -> None:
        """
        为视频添加字幕
        
        Args:
            video_path: 输入视频文件路径
            subtitle_path: 字幕文件路径（支持srt、ass等格式）
            output_path: 输出视频文件路径
            embed: 是否将字幕嵌入到视频中（硬字幕），默认为False（软字幕）
            
        Raises:
            FileException: 文件不存在或添加失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 添加软字幕（可在播放器中开关）
            >>> handler.add_subtitle('input.mp4', 'subtitle.srt', 'output.mp4')
            >>> # 添加硬字幕（永久显示在视频上）
            >>> handler.add_subtitle('input.mp4', 'subtitle.srt', 'output.mp4', embed=True)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        subtitle_path = Path(subtitle_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        if not subtitle_path.exists():
            raise FileException(f"字幕文件不存在: {subtitle_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            if embed:
                # 硬字幕（嵌入到视频中）
                cmd = [
                    "ffmpeg", "-y",
                    "-i", str(video_path),
                    "-vf", f"subtitles={subtitle_path}",
                    "-c:a", "copy",
                    str(output_path)
                ]
            else:
                # 软字幕（作为单独的字幕流）
                cmd = [
                    "ffmpeg", "-y",
                    "-i", str(video_path),
                    "-i", str(subtitle_path),
                    "-c", "copy",
                    "-c:s", "mov_text",  # 使用MP4兼容的字幕格式
                    str(output_path)
                ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频添加字幕成功: {video_path} + {subtitle_path} -> {output_path} (嵌入: {embed})")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"添加视频字幕失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def add_watermark(self, video_path: Union[str, Path], watermark_path: Union[str, Path], 
                    output_path: Union[str, Path], position: str = "bottomright", 
                    opacity: float = 0.5) -> None:
        """
        为视频添加水印
        
        Args:
            video_path: 输入视频文件路径
            watermark_path: 水印图像文件路径
            output_path: 输出视频文件路径
            position: 水印位置，可选"topleft"、"topright"、"bottomleft"、"bottomright"或"center"，默认为"bottomright"
            opacity: 水印不透明度（0.0-1.0），默认为0.5
            
        Raises:
            FileException: 文件不存在或添加失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> handler.add_watermark('input.mp4', 'logo.png', 'output.mp4')
            >>> # 自定义水印位置和不透明度
            >>> handler.add_watermark('input.mp4', 'logo.png', 'output.mp4', position='topleft', opacity=0.3)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        watermark_path = Path(watermark_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        if not watermark_path.exists():
            raise FileException(f"水印图像文件不存在: {watermark_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 确定水印位置
            position_map = {
                "topleft": "10:10",
                "topright": "main_w-overlay_w-10:10",
                "bottomleft": "10:main_h-overlay_h-10",
                "bottomright": "main_w-overlay_w-10:main_h-overlay_h-10",
                "center": "(main_w-overlay_w)/2:(main_h-overlay_h)/2"
            }
            
            if position not in position_map:
                raise ValueError(f"不支持的水印位置: {position}")
            
            position_expr = position_map[position]
            
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-i", str(watermark_path),
                "-filter_complex", f"[1:v]format=rgba,colorchannelmixer=aa={opacity}[watermark];[0:v][watermark]overlay={position_expr}",
                "-c:a", "copy",
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频添加水印成功: {video_path} + {watermark_path} -> {output_path} (位置: {position})")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"添加视频水印失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_gif(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                 start_time: float = 0, duration: float = 3, fps: int = 10, 
                 width: int = 320, quality: int = 90) -> None:
        """
        从视频创建GIF动画
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出GIF文件路径
            start_time: 起始时间（秒），默认为0
            duration: 持续时间（秒），默认为3
            fps: 帧率，默认为10
            width: 输出GIF宽度，默认为320（高度按比例自动计算）
            quality: 质量（1-100），默认为90
            
        Raises:
            FileException: 文件不存在或创建失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> handler.create_gif('input.mp4', 'output.gif')
            >>> # 自定义参数
            >>> handler.create_gif('input.mp4', 'output.gif', start_time=5, duration=2, fps=15, width=480)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 创建临时目录
            temp_dir = tempfile.mkdtemp()
            palette_path = Path(temp_dir) / "palette.png"
            
            # 生成调色板（提高GIF质量）
            cmd1 = [
                "ffmpeg", "-y",
                "-ss", str(start_time),
                "-t", str(duration),
                "-i", str(video_path),
                "-vf", f"fps={fps},scale={width}:-1:flags=lanczos,palettegen",
                str(palette_path)
            ]
            
            # 使用调色板创建GIF
            cmd2 = [
                "ffmpeg", "-y",
                "-ss", str(start_time),
                "-t", str(duration),
                "-i", str(video_path),
                "-i", str(palette_path),
                "-filter_complex", f"fps={fps},scale={width}:-1:flags=lanczos[x];[x][1:v]paletteuse",
                "-loop", "0",  # 0表示无限循环
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            subprocess.run(cmd1, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            subprocess.run(cmd2, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            # 清理临时文件
            try:
                os.remove(palette_path)
                os.rmdir(temp_dir)
            except Exception:
                pass  # 忽略清理错误
            
            logger.info(f"从视频创建GIF成功: {video_path} -> {output_path} (时长: {duration}s, 帧率: {fps})")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"从视频创建GIF失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def apply_video_filter(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                         filter_type: str, filter_params: Dict[str, Any] = None) -> None:
        """
        应用视频滤镜
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            filter_type: 滤镜类型，支持'blur'、'sharpen'、'grayscale'、'rotate'、'flip'等
            filter_params: 滤镜参数，默认为None
            
        Raises:
            FileException: 文件不存在或处理失败时抛出
            ValueError: 不支持的滤镜类型时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 应用模糊滤镜
            >>> handler.apply_video_filter('input.mp4', 'output.mp4', 'blur', {'strength': 5})
            >>> # 应用灰度滤镜
            >>> handler.apply_video_filter('input.mp4', 'output.mp4', 'grayscale')
            >>> # 旋转视频
            >>> handler.apply_video_filter('input.mp4', 'output.mp4', 'rotate', {'angle': 90})
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        # 如果未提供滤镜参数，使用空字典
        if filter_params is None:
            filter_params = {}
        
        try:
            # 构建ffmpeg滤镜字符串
            filter_str = ""
            if filter_type == 'blur':
                strength = filter_params.get('strength', 5)
                filter_str = f"boxblur={strength}:{strength}:1"
            elif filter_type == 'sharpen':
                strength = filter_params.get('strength', 1.0)
                filter_str = f"unsharp=5:5:{strength}:5:5:{strength}"
            elif filter_type == 'grayscale':
                filter_str = "format=gray"
            elif filter_type == 'rotate':
                angle = filter_params.get('angle', 90)
                # 根据角度选择旋转方式
                if angle == 90:
                    filter_str = "transpose=1"
                elif angle == 180:
                    filter_str = "transpose=1,transpose=1"
                elif angle == 270:
                    filter_str = "transpose=2"
                else:
                    filter_str = f"rotate={angle}*PI/180"
            elif filter_type == 'flip':
                direction = filter_params.get('direction', 'horizontal')
                if direction == 'horizontal':
                    filter_str = "hflip"
                elif direction == 'vertical':
                    filter_str = "vflip"
                else:
                    raise ValueError(f"不支持的翻转方向: {direction}，支持的方向有'horizontal'和'vertical'")
            elif filter_type == 'brightness':
                value = filter_params.get('value', 0.1)
                filter_str = f"eq=brightness={value}"
            elif filter_type == 'contrast':
                value = filter_params.get('value', 1.2)
                filter_str = f"eq=contrast={value}"
            elif filter_type == 'saturation':
                value = filter_params.get('value', 1.5)
                filter_str = f"eq=saturation={value}"
            elif filter_type == 'noise':
                strength = filter_params.get('strength', 10)
                filter_str = f"noise=alls={strength}:allf=t"
            elif filter_type == 'sepia':
                filter_str = "colorchannelmixer=.393:.769:.189:0:.349:.686:.168:0:.272:.534:.131"
            elif filter_type == 'mirror':
                filter_str = "crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left][right]hstack"
            else:
                raise ValueError(f"不支持的滤镜类型: {filter_type}")
            
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-vf", filter_str,
                "-c:a", "copy",  # 复制音频流
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频滤镜应用成功: {video_path} -> {output_path} (滤镜: {filter_type})")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"应用视频滤镜失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def change_video_speed(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                         speed_factor: float, preserve_audio_pitch: bool = False) -> None:
        """
        改变视频播放速度
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            speed_factor: 速度因子，大于1加速，小于1减速
            preserve_audio_pitch: 是否保持音频音调不变，默认为False
            
        Raises:
            FileException: 文件不存在或处理失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 加速视频（2倍速）
            >>> handler.change_video_speed('input.mp4', 'output.mp4', 2.0)
            >>> # 减速视频（0.5倍速）并保持音频音调
            >>> handler.change_video_speed('input.mp4', 'output.mp4', 0.5, preserve_audio_pitch=True)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        if speed_factor <= 0:
            raise ValueError("速度因子必须大于0")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 构建ffmpeg命令
            # 视频速度滤镜
            video_filter = f"setpts={1/speed_factor}*PTS"
            
            # 音频速度滤镜
            if preserve_audio_pitch:
                # 保持音调不变
                audio_filter = f"atempo={speed_factor}"
                # atempo滤镜限制：0.5 <= speed_factor <= 2.0
                # 对于超出范围的值，需要串联多个atempo滤镜
                if speed_factor > 2.0:
                    # 例如，speed_factor=4.0可以拆分为2.0*2.0
                    iterations = 0
                    temp_factor = speed_factor
                    while temp_factor > 2.0:
                        temp_factor /= 2.0
                        iterations += 1
                    audio_filter = ",".join(["atempo=2.0"] * iterations)
                    if temp_factor > 1.0:
                        audio_filter += f",atempo={temp_factor}"
                elif speed_factor < 0.5:
                    # 例如，speed_factor=0.25可以拆分为0.5*0.5
                    iterations = 0
                    temp_factor = speed_factor
                    while temp_factor < 0.5:
                        temp_factor *= 2.0
                        iterations += 1
                    audio_filter = ",".join(["atempo=0.5"] * iterations)
                    if temp_factor < 1.0:
                        audio_filter += f",atempo={temp_factor}"
            else:
                # 不保持音调
                audio_filter = f"asetrate=44100*{speed_factor},aresample=44100"
            
            # 构建完整命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-filter_complex", f"[0:v]{video_filter}[v];[0:a]{audio_filter}[a]",
                "-map", "[v]",
                "-map", "[a]",
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"视频速度改变成功: {video_path} -> {output_path} (速度因子: {speed_factor})")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"改变视频速度失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def compress_video(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                      crf: int = 23, preset: str = "medium", codec: str = "libx264") -> None:
        """
        压缩视频
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            crf: 恒定速率因子（0-51，值越大压缩率越高，质量越低），默认为23
            preset: 编码预设，可选"ultrafast"、"superfast"、"veryfast"、"faster"、"fast"、"medium"、"slow"、"slower"、"veryslow"，默认为"medium"
            codec: 视频编解码器，默认为"libx264"（H.264）
            
        Raises:
            FileException: 文件不存在或压缩失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 使用默认参数压缩
            >>> handler.compress_video('input.mp4', 'output.mp4')
            >>> # 高压缩率，低质量
            >>> handler.compress_video('input.mp4', 'output.mp4', crf=28, preset='fast')
            >>> # 低压缩率，高质量
            >>> handler.compress_video('input.mp4', 'output.mp4', crf=18, preset='slow')
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-c:v", codec,
                "-crf", str(crf),
                "-preset", preset,
                "-c:a", "aac",
                "-b:a", "128k",
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            # 获取原始文件和压缩后文件的大小
            original_size = video_path.stat().st_size
            compressed_size = output_path.stat().st_size
            compression_ratio = original_size / compressed_size if compressed_size > 0 else 0
            
            logger.info(f"视频压缩成功: {video_path} -> {output_path} (压缩比: {compression_ratio:.2f}x)")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"压缩视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_timelapse(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                       speed_factor: int = 10, fps: float = None) -> None:
        """
        从视频创建延时摄影（缩时）视频
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出视频文件路径
            speed_factor: 加速因子，默认为10（10倍速）
            fps: 输出视频帧率，默认为None（保持原帧率）
            
        Raises:
            FileException: 文件不存在或处理失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 创建10倍速延时视频
            >>> handler.create_timelapse('input.mp4', 'timelapse.mp4')
            >>> # 创建20倍速延时视频并设置帧率
            >>> handler.create_timelapse('input.mp4', 'timelapse.mp4', speed_factor=20, fps=30)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 获取视频信息
            video_info = self.get_video_info(video_path)
            original_fps = video_info['fps']
            
            # 如果未指定输出帧率，使用原帧率
            if fps is None:
                fps = original_fps
            
            # 计算帧采样率（每speed_factor帧取1帧）
            frame_step = speed_factor
            
            # 构建ffmpeg命令
            cmd = [
                "ffmpeg", "-y",
                "-i", str(video_path),
                "-vf", f"setpts=PTS/{speed_factor},fps={fps}",  # 调整时间戳和帧率
                "-an",  # 移除音频
                "-c:v", "libx264",
                "-crf", "23",
                "-preset", "medium",
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            logger.info(f"延时视频创建成功: {video_path} -> {output_path} (加速: {speed_factor}x)")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"创建延时视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_slideshow(self, image_paths: List[Union[str, Path]], output_path: Union[str, Path], 
                       duration: float = 3.0, transition: str = "fade", 
                       transition_duration: float = 1.0, fps: int = 30) -> None:
        """
        从图像创建幻灯片视频
        
        Args:
            image_paths: 输入图像文件路径列表
            output_path: 输出视频文件路径
            duration: 每张图像显示的时长（秒），默认为3.0
            transition: 转场效果，可选"fade"、"wipe"、"slide"等，默认为"fade"
            transition_duration: 转场时长（秒），默认为1.0
            fps: 输出视频帧率，默认为30
            
        Raises:
            FileException: 文件不存在或处理失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 创建简单幻灯片
            >>> handler.create_slideshow(['image1.jpg', 'image2.jpg', 'image3.jpg'], 'slideshow.mp4')
            >>> # 自定义参数
            >>> handler.create_slideshow(['image1.jpg', 'image2.jpg'], 'slideshow.mp4', 
            ...                        duration=5.0, transition='wipe', transition_duration=0.5)
        """
        # 检查ffmpeg是否可用
        try:
            import subprocess
            subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        except (subprocess.CalledProcessError, FileNotFoundError):
            raise FileException("未找到ffmpeg，请确保已安装并添加到PATH环境变量")
        
        if not image_paths:
            raise FileException("图像文件路径列表不能为空")
        
        # 检查所有图像文件是否存在
        for path in image_paths:
            path = Path(path)
            if not path.exists():
                raise FileException(f"图像文件不存在: {path}")
        
        output_path = Path(output_path)
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 创建临时目录
            temp_dir = tempfile.mkdtemp()
            
            # 创建临时文件列表
            list_file_path = Path(temp_dir) / "image_list.txt"
            
            # 计算总时长和转场参数
            total_duration = len(image_paths) * duration
            
            # 写入文件列表
            with open(list_file_path, "w") as f:
                for path in image_paths:
                    # 对于每个图像，指定持续时间
                    f.write(f"file '{Path(path).absolute()}\n'")
                    f.write(f"duration {duration}\n")
            
            # 构建ffmpeg命令
            # 根据转场类型选择不同的滤镜
            if transition == "fade":
                transition_filter = f"xfade=transition=fade:duration={transition_duration}:offset={duration-transition_duration}"
            elif transition == "wipe":
                transition_filter = f"xfade=transition=wiperight:duration={transition_duration}:offset={duration-transition_duration}"
            elif transition == "slide":
                transition_filter = f"xfade=transition=slideright:duration={transition_duration}:offset={duration-transition_duration}"
            else:
                # 默认使用淡入淡出
                transition_filter = f"xfade=transition=fade:duration={transition_duration}:offset={duration-transition_duration}"
            
            cmd = [
                "ffmpeg", "-y",
                "-f", "concat",
                "-safe", "0",
                "-i", str(list_file_path),
                "-vf", f"fps={fps},format=yuv420p",  # 确保兼容性
                "-c:v", "libx264",
                "-crf", "23",
                "-preset", "medium",
                str(output_path)
            ]
            
            # 执行ffmpeg命令
            process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            
            # 清理临时文件
            try:
                os.remove(list_file_path)
                os.rmdir(temp_dir)
            except Exception:
                pass  # 忽略清理错误
            
            logger.info(f"幻灯片视频创建成功: {len(image_paths)} 张图像 -> {output_path} (总时长: {total_duration}s)")
        except subprocess.CalledProcessError as e:
            raise FileException(f"ffmpeg处理失败: {e.stderr.decode() if e.stderr else ''}")
        except Exception as e:
            raise FileException(f"创建幻灯片视频失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=FileException)
    def create_video_thumbnail(self, video_path: Union[str, Path], output_path: Union[str, Path], 
                             time_position: float = None, width: int = 320, 
                             quality: int = 90) -> None:
        """
        从视频创建缩略图
        
        Args:
            video_path: 输入视频文件路径
            output_path: 输出缩略图文件路径
            time_position: 提取缩略图的时间位置（秒），默认为None（视频中间位置）
            width: 输出缩略图宽度，默认为320（高度按比例自动计算）
            quality: 图像质量（1-100），默认为90
            
        Raises:
            FileException: 文件不存在或处理失败时抛出
        
        Examples:
            >>> handler = VideoFileHandler()
            >>> # 从视频中间位置创建缩略图
            >>> handler.create_video_thumbnail('input.mp4', 'thumbnail.jpg')
            >>> # 从指定时间位置创建缩略图
            >>> handler.create_video_thumbnail('input.mp4', 'thumbnail.jpg', time_position=10.5)
        """
        video_path = Path(video_path)
        output_path = Path(output_path)
        
        if not video_path.exists():
            raise FileException(f"视频文件不存在: {video_path}")
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 获取视频信息
            video_info = self.get_video_info(video_path)
            
            # 如果未指定时间位置，使用视频中间位置
            if time_position is None:
                time_position = video_info['duration'] / 2
            
            # 确保时间位置在视频范围内
            if time_position < 0 or time_position > video_info['duration']:
                time_position = min(max(0, time_position), video_info['duration'])
            
            # 加载视频
            video = self.load_video(video_path)
            
            # 设置视频位置到指定时间
            video.set(self.cv2.CAP_PROP_POS_MSEC, time_position * 1000)
            
            # 读取帧
            ret, frame = video.read()
            if not ret:
                raise FileException(f"无法读取视频帧，时间位置: {time_position}秒")
            
            # 调整大小
            if width is not None:
                height = int(frame.shape[0] * (width / frame.shape[1]))
                frame = self.cv2.resize(frame, (width, height))
            
            # 保存帧为图像
            success = self.cv2.imwrite(str(output_path), frame, [self.cv2.IMWRITE_JPEG_QUALITY, quality])
            if not success:
                raise FileException(f"无法保存缩略图到{output_path}")
            
            # 释放视频对象
            video.release()
            
            logger.info(f"视频缩略图创建成功: {video_path} -> {output_path} (时间位置: {time_position}s)")
        except Exception as e:
            raise FileException(f"创建视频缩略图失败: {e}")